﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace MemoryViewControl
{
  public partial class MemoryHeatMapControl : UserControl
  {
    public event PropertyChangedEventHandler PropertyChanged;

    private MemorySpace m_memorySpace = new MemorySpace();

    private Point m_mousePos = new Point();
    private PointF m_renderOffset = new Point();

    private float m_scale = 1.0f;
    private int m_width = 256;
    private int m_height = 0;

    private uint m_offset = 0;
    private uint m_selectionStart = 0;
    private uint m_selectionEnd = 0;

    private float m_border = 64.0f;

    #region Properties

    public int HeatMapWidth
    {
      get { return m_width; }
      set { if (m_width != value) { m_width = value; OnPropertyChanged("HeatMapWidth"); RedrawMemory(true); } }
    }

    public float HeatMapScale
    {
      get { return m_scale; }
      set { if (m_scale != value) { m_scale = value; OnPropertyChanged("HeatMapScale"); RedrawMemory(true); } }
    }

    public float Border
    {
      get { return m_border; }
      set { if (m_border != value) { m_border = value; OnPropertyChanged("Border"); RedrawMemory(true); } }
    }

    public MemorySpace MemorySpace
    {
      get { return m_memorySpace; }
      set
      {
        if (m_memorySpace != value)
        {
          if (m_memorySpace != null)
          {
            m_memorySpace.MemoryChanged -= new EventHandler(MemorySpace_MemoryChanged);
          }

          m_memorySpace = value;

          if (m_memorySpace != null)
          {
            m_memorySpace.MemoryChanged += new EventHandler(MemorySpace_MemoryChanged);
          }

          OnPropertyChanged("MemorySpace");
        }
      }
    }
    #endregion

    public MemoryHeatMapControl()
    {
      DoubleBuffered = true;
      SetStyle(ControlStyles.Selectable, true);
      SetStyle(ControlStyles.UserMouse, true);
      Font = new Font(FontFamily.GenericMonospace, 12, FontStyle.Regular);

      InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);

      CalculateSizes();

      ClampRenderOffset();
    }
  
    private void MemorySpace_MemoryChanged(object sender, EventArgs args)
    {
      RedrawMemory(true);
    }

    protected void OnPropertyChanged(string propertyName)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
    }

    public void CalculateSizes()
    {
      if (m_memorySpace != null)
      {
        m_height = (int)(m_memorySpace.AddressMax / (ulong)m_width);
      }
      else
      {
        m_height = 0;
      }

      UpdateScrollBarPosition();
    }

    public void RedrawMemory(bool dirty)
    {
      if (dirty)
        UpdateScrollBarPosition();
      Invalidate();
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
      base.OnPaint(pe);

      CalculateSizes();

      Graphics graphics = pe.Graphics;

      int height = (int)graphics.VisibleClipBounds.Height;
      float width = graphics.VisibleClipBounds.Width;

      graphics.Clear(Color.Black);

      if (m_memorySpace == null)
      {
        graphics.DrawString("No memory", Font, Brushes.White, 0, 0);
        return;
      }

      int lines = Math.Min((int)(height / m_scale), (int)(m_memorySpace.AddressMax / (ulong)m_width));
      //int lines = (int)(m_memorySpace.AddressMax / (ulong)m_width);

      int startingLine = (m_renderOffset.Y < 0) ? (int)(-m_renderOffset.Y / m_scale) : 0;
      int maxLine = startingLine + lines;

      // white outline
      graphics.DrawRectangle(Pens.White, m_renderOffset.X - 1, m_renderOffset.Y - 1, (m_width * m_scale) + 2, (m_height * m_scale) + 2);

      MemorySpace.ByteInfo[] bytes = new MemorySpace.ByteInfo[m_width];

      for (int y = startingLine; y < maxLine; ++y)
      {
        float cellY = y * m_scale;

        if ((cellY + m_renderOffset.Y + m_scale) < 0)
          continue;

        if ((cellY + m_renderOffset.Y) > height)
          break;

        uint lineAddress = (uint)(y * m_width);
        m_memorySpace.GetBytes(lineAddress, ref bytes);

        for (int x = 0; x < m_width; ++x)
        {
          float cellX = x * m_scale;

          if ((cellX + m_renderOffset.X + m_scale) < 0)
            continue;

          if ((cellX + m_renderOffset.X) > width)
            break;

          uint mem = (uint)((y * m_width) + x);
          MemorySpace.ByteInfo bi = bytes[x];

          if ((bi.writeHeat != 0) || (bi.readHeat != 0) || (bi.bankHeat != 0))
          {
            Color colour = Color.FromArgb(bi.writeHeat, bi.readHeat, bi.bankHeat);
            //Pen pen = new Pen(colour);
            using (Brush brush = new SolidBrush(colour))
            {
              graphics.FillRectangle(brush, cellX + m_renderOffset.X, cellY + m_renderOffset.Y, m_scale, m_scale);
            }
          }
        }
      }

      DrawHoverInfo(graphics);
    }

    private void DrawHoverInfo(Graphics graphics)
    {
      Point cell = GetCurrentCell();

      if ((cell.X >= 0) && (cell.X < m_width) && (cell.Y >= 0) && (cell.Y < m_height))
      {
        uint selectionAddress = ((uint)cell.Y * (uint)m_width) + (uint)cell.X;

        float selectionCellX = m_renderOffset.X + (cell.X * m_scale);
        float selectionCellY = m_renderOffset.Y + (cell.Y * m_scale);

        graphics.DrawRectangle(Pens.White, selectionCellX - 1, selectionCellY - 1, m_scale + 1, m_scale + 1);

        MemorySpace.ByteInfo bi = m_memorySpace.GetByteSlow(selectionAddress);
        string hoverText = string.Format("0x{0:X4} (0x{1:X2})", selectionAddress, bi.bits);

        graphics.DrawString(hoverText, Font, Brushes.Black, selectionCellX + 1, selectionCellY + m_scale + 1);

        graphics.DrawString(hoverText, Font, Brushes.AntiqueWhite, selectionCellX, selectionCellY + m_scale);
      }
    }

    void ClampRenderOffset()
    {
      m_renderOffset.X = Math.Max(m_renderOffset.X, ClientSize.Width - (m_width * m_scale) - m_border);
      m_renderOffset.Y = Math.Max(m_renderOffset.Y, ClientSize.Height - (m_height * m_scale) - m_border);

      m_renderOffset.X = Math.Min(0.0f + m_border, m_renderOffset.X);
      m_renderOffset.Y = Math.Min(0.0f + m_border, m_renderOffset.Y);
    }

    public Point GetCurrentCell()
    {
      int nCellX = (int)((m_mousePos.X - m_renderOffset.X) / m_scale);
      int nCellY = (int)((m_mousePos.Y - m_renderOffset.Y) / m_scale);

      return new Point(nCellX, nCellY);
    }

    public void CenterOnCell(Point cell, Point center)
    {
      float fx = (cell.X * m_scale) + (m_scale / 2.0f) - (float)center.X;
      float fy = (cell.Y * m_scale) + (m_scale / 2.0f) - (float)center.Y;

      m_renderOffset.X = -fx;
      m_renderOffset.Y = -fy;

      ClampRenderOffset();
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
      base.OnMouseMove(e);

      int deltaX = m_mousePos.X - e.Location.X;
      int deltaY = m_mousePos.Y - e.Location.Y;

      m_mousePos.X = e.X;
      m_mousePos.Y = e.Y;

      // TODO: check if cell the mouse is over has changed and redraw
      RedrawMemory(true);

      if ((e.Button & MouseButtons.Right) != 0)
      {
        int multiplier = ((ModifierKeys & Keys.Control) != 0) ?  4 : 1;

        m_renderOffset.X -= (deltaX * multiplier);
        m_renderOffset.Y -= (deltaY * multiplier);

        ClampRenderOffset();

        RedrawMemory(true);
      }
      
      if ((e.Button & MouseButtons.Left) != 0)
      {
        m_selectionStart = 0;
        m_selectionEnd = 0;

        RedrawMemory(true);
      }
    }

    protected override void OnMouseWheel(MouseEventArgs e)
    {
      base.OnMouseWheel(e);

      if (e.Delta != 0)
      {
        Point currentCell = GetCurrentCell();

        float scale = e.Delta / SystemInformation.MouseWheelScrollDelta;

        if (scale > 0)
        {
          m_scale *= (float)Math.Pow(2, scale);
        }
        else
        {
          m_scale /= (float)Math.Pow(2, -scale);
        }

        m_scale = Math.Min(m_scale, 64.0f);
        m_scale = Math.Max(m_scale, 1.0f);

        CenterOnCell(currentCell, m_mousePos);

        RedrawMemory(true);
      }
    }

    protected override bool IsInputKey(Keys keyData)
    {
      if ((keyData & Keys.Alt) == Keys.Alt)
      {
        return false;
      }

      switch ((keyData & Keys.KeyCode))
      {
      case Keys.Left:
      case Keys.Up:
      case Keys.Right:
      case Keys.Down:
      case Keys.Prior:
      case Keys.Next:
      case Keys.End:
      case Keys.Home:
      case Keys.Escape:
        return true;
      }

      return base.IsInputKey(keyData);
    }

    protected override void OnKeyDown(KeyEventArgs e)
    {
      base.OnKeyDown(e);

      switch (e.KeyCode)
      {
      case Keys.Home:
        m_mousePos = Point.Empty;
        m_renderOffset = Point.Empty;
        ClampRenderOffset();
        m_scale = 1.0f;
        break;
      }
    }

    private void scrollBarMemory_Scroll(object sender, ScrollEventArgs e)
    {
      m_renderOffset.Y = -((float)e.NewValue) + m_border;
      RedrawMemory(true);
    }

    private void UpdateScrollBarPosition()
    {
      int scrollHeight = (int)(m_border + (m_height * m_scale) + m_border);
      int pageHeight = (int)((float)ClientSize.Height);

      scrollBarMemory.Maximum = scrollHeight;
      scrollBarMemory.LargeChange = pageHeight;

      float renderOffset = -m_renderOffset.Y + m_border;
      //Console.WriteLine("renderOffset:{0}", renderOffset);
      m_offset = (uint)(renderOffset);
      int newScrollValue = Math.Min(scrollBarMemory.Maximum, (int)(m_offset));
      scrollBarMemory.Value = newScrollValue;
    }
  }
}
